import Vue from "../../../utils/vue";
import {
  getAttr,
  hasAttr,
  removeAttr,
  setAttr,
  addClass,
  removeClass,
  getBCR,
  getCS,
  selectAll,
  requestAF
} from "../../../utils/dom";
import { isBrowser } from "../../../utils/env";
import { isNull } from "../../../utils/inspect";
import { toFloat, toInteger } from "../../../utils/number";

const DEFAULT_ZINDEX = 1040;

const Selector = {
  FIXED_CONTENT: ".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",
  STICKY_CONTENT: ".sticky-top",
  NAVBAR_TOGGLER: ".navbar-toggler"
};

const ModalManager = Vue.extend({
  data() {
    return {
      modals: [],
      baseZIndex: null,
      scrollbarWidth: null,
      isBodyOverflowing: false
    };
  },
  computed: {
    modalCount() {
      return this.modals.length;
    },
    modalsAreOpen() {
      return this.modalCount > 0;
    }
  },
  watch: {
    modalCount(newCount, oldCount) {
      if (isBrowser) {
        this.getScrollbarWidth();
        if (newCount > 0 && oldCount === 0) {
          // Transitioning to modal(s) open
          this.checkScrollbar();
          this.setScrollbar();
          addClass(document.body, "modal-open");
        } else if (newCount === 0 && oldCount > 0) {
          // Transitioning to modal(s) closed
          this.resetScrollbar();
          removeClass(document.body, "modal-open");
        }
        setAttr(document.body, "data-modal-open-count", String(newCount));
      }
    },
    modals(newVal) {
      this.checkScrollbar();
      requestAF(() => {
        this.updateModals(newVal || []);
      });
    }
  },
  methods: {
    // Public methods
    registerModal(modal) {
      // Register the modal if not already registered
      if (modal && this.modals.indexOf(modal) === -1) {
        // Add modal to modals array
        this.modals.push(modal);
        modal.$once("hook:beforeDestroy", () => {
          this.unregisterModal(modal);
        });
      }
    },
    unregisterModal(modal) {
      const index = this.modals.indexOf(modal);
      if (index > -1) {
        // Remove modal from modals array
        this.modals.splice(index, 1);
        // Reset the modal's data
        if (!(modal._isBeingDestroyed || modal._isDestroyed)) {
          this.resetModal(modal);
        }
      }
    },
    getBaseZIndex() {
      if (isNull(this.baseZIndex) && isBrowser) {
        // Create a temporary `div.modal-backdrop` to get computed z-index
        const div = document.createElement("div");
        div.className = "modal-backdrop d-none";
        div.style.display = "none";
        document.body.appendChild(div);
        this.baseZIndex = toInteger(getCS(div).zIndex, DEFAULT_ZINDEX);
        document.body.removeChild(div);
      }
      return this.baseZIndex || DEFAULT_ZINDEX;
    },
    getScrollbarWidth() {
      if (isNull(this.scrollbarWidth) && isBrowser) {
        // Create a temporary `div.measure-scrollbar` to get computed z-index
        const div = document.createElement("div");
        div.className = "modal-scrollbar-measure";
        document.body.appendChild(div);
        this.scrollbarWidth = getBCR(div).width - div.clientWidth;
        document.body.removeChild(div);
      }
      return this.scrollbarWidth || 0;
    },
    // Private methods
    updateModals(modals) {
      const baseZIndex = this.getBaseZIndex();
      const scrollbarWidth = this.getScrollbarWidth();
      modals.forEach((modal, index) => {
        // We update data values on each modal
        modal.zIndex = baseZIndex + index;
        modal.scrollbarWidth = scrollbarWidth;
        modal.isTop = index === this.modals.length - 1;
        modal.isBodyOverflowing = this.isBodyOverflowing;
      });
    },
    resetModal(modal) {
      if (modal) {
        modal.zIndex = this.getBaseZIndex();
        modal.isTop = true;
        modal.isBodyOverflowing = false;
      }
    },
    checkScrollbar() {
      // Determine if the body element is overflowing
      const { left, right } = getBCR(document.body);
      this.isBodyOverflowing = left + right < window.innerWidth;
    },
    setScrollbar() {
      const body = document.body;
      // Storage place to cache changes to margins and padding
      // Note: This assumes the following element types are not added to the
      // document after the modal has opened.
      body._paddingChangedForModal = body._paddingChangedForModal || [];
      body._marginChangedForModal = body._marginChangedForModal || [];
      if (this.isBodyOverflowing) {
        const scrollbarWidth = this.scrollbarWidth;
        // Adjust fixed content padding
        /* istanbul ignore next: difficult to test in JSDOM */
        selectAll(Selector.FIXED_CONTENT).forEach(el => {
          const actualPadding = el.style.paddingRight;
          setAttr(el, "data-padding-right", actualPadding);
          el.style.paddingRight = `${toFloat(getCS(el).paddingRight, 0) +
            scrollbarWidth}px`;
          body._paddingChangedForModal.push(el);
        });
        // Adjust sticky content margin
        /* istanbul ignore next: difficult to test in JSDOM */
        selectAll(Selector.STICKY_CONTENT).forEach(
          el => /* istanbul ignore next */ {
            const actualMargin = el.style.marginRight;
            setAttr(el, "data-margin-right", actualMargin);
            el.style.marginRight = `${toFloat(getCS(el).marginRight, 0) -
              scrollbarWidth}px`;
            body._marginChangedForModal.push(el);
          }
        );
        // Adjust <b-navbar-toggler> margin
        /* istanbul ignore next: difficult to test in JSDOM */
        selectAll(Selector.NAVBAR_TOGGLER).forEach(
          el => /* istanbul ignore next */ {
            const actualMargin = el.style.marginRight;
            setAttr(el, "data-margin-right", actualMargin);
            el.style.marginRight = `${toFloat(getCS(el).marginRight, 0) +
              scrollbarWidth}px`;
            body._marginChangedForModal.push(el);
          }
        );
        // Adjust body padding
        const actualPadding = body.style.paddingRight;
        setAttr(body, "data-padding-right", actualPadding);
        body.style.paddingRight = `${toFloat(getCS(body).paddingRight, 0) +
          scrollbarWidth}px`;
      }
    },
    resetScrollbar() {
      const body = document.body;
      if (body._paddingChangedForModal) {
        // Restore fixed content padding
        body._paddingChangedForModal.forEach(el => {
          /* istanbul ignore next: difficult to test in JSDOM */
          if (hasAttr(el, "data-padding-right")) {
            el.style.paddingRight = getAttr(el, "data-padding-right") || "";
            removeAttr(el, "data-padding-right");
          }
        });
      }
      if (body._marginChangedForModal) {
        // Restore sticky content and navbar-toggler margin
        body._marginChangedForModal.forEach(el => {
          /* istanbul ignore next: difficult to test in JSDOM */
          if (hasAttr(el, "data-margin-right")) {
            el.style.marginRight = getAttr(el, "data-margin-right") || "";
            removeAttr(el, "data-margin-right");
          }
        });
      }
      body._paddingChangedForModal = null;
      body._marginChangedForModal = null;
      // Restore body padding
      if (hasAttr(body, "data-padding-right")) {
        body.style.paddingRight = getAttr(body, "data-padding-right") || "";
        removeAttr(body, "data-padding-right");
      }
    }
  }
});

// Create and export our modal manager instance
export const modalManager = new ModalManager();
